package gravity;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.imageio.ImageIO;
public class test
{
	public static class particle implements Serializable
	{
		private static final long serialVersionUID = 4997030901086080857L;
		double[] position = new double[3];
		double[] velocity = new double[3];
		double[] acceleration = new double[3];
		double mass;
		double radius;
		double[] color = new double[3];
	}
	public static class vars
	{
		static int frame = 0, nP = 100000, side = 200, totalFrames = 6000, numProcessors = 8, lastEmpty = nP-1, clipping = 500, width = 1920, height = 1080;
		static double g = 6.67428e-11, density = 1e11, dt = 0.033333, minMass = 1e12, maxMass = 1e13, minSpeed = -50, maxSpeed = 50, convFactor = (Math.sqrt(3)/2);
		static boolean finished = false;
		static particle[] particleList = new particle[nP];
		static ExecutorService executor = Executors.newFixedThreadPool(numProcessors);
	}
	public static double[] vecAdd(double[] a, double[] b)
	{
		double[] c = {a[0]+b[0], a[1]+b[1], a[2]+b[2]};
		return c;
	}
	public static double[] vecSub(double[] a, double[] b)
	{
		double[] c = {a[0]-b[0], a[1]-b[1], a[2]-b[2]};
		return c;
	}
	public static double[] vecMul(double[] a, double b)
	{
		double[] c = {a[0]*b, a[1]*b, a[2]*b};
		return c;
	}
	public static double[] vecDiv(double[] a, double b)
	{
		double[] c = {a[0]/b, a[1]/b, a[2]/b};
		return c;
	}
	public static double sumDeltaSquare(double[] pos1, double[] pos2)
	{
		return Math.pow(pos1[0]-pos2[0],2)+Math.pow(pos1[1]-pos2[1],2)+Math.pow(pos1[2]-pos2[2],2);
	}
	public static double[] getForce(double[] pos1, double m1, double[] pos2, double m2)
	{
		double ratio = ((vars.g*(m1*m2))/(sumDeltaSquare(pos1, pos2)))/Math.sqrt(sumDeltaSquare(pos1,pos2));
		return vecMul(vecSub(pos2, pos1), ratio);
	}
	public static int getFirstEmpty()
	{
		for (int index = 0; index < vars.nP; index++)
		{
			if (vars.particleList[index].mass == 0)
			{
				return index;
			}
		}
		return -1;
	}
	public static int getLastFull()
	{
		for (int index = vars.lastEmpty; index >= 0; index--)
		{
			if (vars.particleList[index].mass != 0)
			{
				return index;
			}
		}
		return -1;
	}
	public static void updateLastFull()
	{
		if (getLastFull() != -1)
		{
			vars.lastEmpty = getLastFull()+1;
		}
	}
	public static void arrange()
	{
		for (int index = 0; index <= getLastFull(); index++)
		{
			if (vars.particleList[index].mass == 0)
			{
				System.out.println("Populated "+index);
				vars.particleList[index] = vars.particleList[getLastFull()];
				vars.particleList[getLastFull()] = new particle();
				updateLastFull();
			}
		}
	}
	public static void addParticle(double[] position, double[] velocity, double mass, double radius, double[] color)
	{
		int index = getFirstEmpty();
		vars.particleList[index].position = position;
		vars.particleList[index].velocity = velocity;
		vars.particleList[index].acceleration = new double[3];
		vars.particleList[index].mass = mass;
		vars.particleList[index].radius = radius;
		vars.particleList[index].color = color;
	}
	public static void removeParticle(int index)
	{
		vars.particleList[index] = new particle();
	}
	public static void resetAcc()
	{
		for (int particle = 0; particle <= getLastFull(); particle++)
		{
			vars.particleList[particle].acceleration = new double[3];
		}
	}
	public static void initList()
	{
		for (int index = 0; index < vars.nP; index++)
		{
			if (index%(vars.nP/10)==0)
			{
				System.out.println(index/(vars.nP/100)+"%");
			}
			vars.particleList[index] = new particle();
		}
	}
	public static void populateList()
	{
		for (int index = 0; index < vars.nP; index++)
		{
			if (index%(vars.nP/10)==0)
			{
				System.out.println(index/(vars.nP/100)+"%");
			}
			double[] pos = {random(-vars.side,vars.side),random(-vars.side,vars.side),random(-vars.side,vars.side)};
			double[] vel = {random(vars.minSpeed, vars.maxSpeed),random(vars.minSpeed, vars.maxSpeed),random(vars.minSpeed, vars.maxSpeed)};
			double[] col = {random(0,255), random(0,255), random(0,255)};
			double m = random(vars.minMass,vars.maxMass);
			double r = Math.cbrt((3*m)/(4*Math.PI*vars.density));
			vars.particleList[index].position = pos;
			vars.particleList[index].velocity = vel;
			vars.particleList[index].color = col;
			vars.particleList[index].mass = m;
			vars.particleList[index].radius = r;
		}
	}
	private static class updateAcc implements Runnable
	{
		private int p1;
		public updateAcc(int p)
		{
			p1 = p;
		}
		public void run()
		{
			for (int p2 = p1+1; p2 < getLastFull(); p2++)
			{
				double[] f1 = getForce(vars.particleList[p1].position, vars.particleList[p1].mass, vars.particleList[p2].position, vars.particleList[p2].mass);
				double[] f2 = vecMul(f1, -1);
				vars.particleList[p1].acceleration = vecAdd(vars.particleList[p1].acceleration, vecDiv(f1, vars.particleList[p1].mass));
				vars.particleList[p2].acceleration = vecAdd(vars.particleList[p2].acceleration, vecDiv(f2, vars.particleList[p2].mass));
			}
			if (p1 % (vars.nP/100) == 0)
			{
				System.out.println(p1+"|"+getLastFull());
			}
		}
	}
	private static class updateVel implements Runnable
	{
		private int particle;
		public updateVel(int p)
		{
			particle = p;
		}
		public void run()
		{
			if (vars.particleList[particle].mass != 0)
			{
				vars.particleList[particle].velocity = vecAdd(vecMul(vars.particleList[particle].acceleration, vars.dt), vars.particleList[particle].velocity);
			}
		}
	}
	private static class updatePos implements Runnable
	{
		private int particle;
		public updatePos(int p)
		{
			particle = p;
		}
		public void run()
		{
			if (vars.particleList[particle].mass != 0)
			{
				vars.particleList[particle].position = vecAdd(vecMul(vars.particleList[particle].velocity, vars.dt), vars.particleList[particle].position);
			}
		}
	}
	public static double distance(double[] v1, double[] v2)
	{
		return Math.sqrt(Math.pow(v1[0]-v2[0],2) + Math.pow(v1[1]-v2[1],2) + Math.pow(v1[2]-v2[2],2));
	}
	public static double random(double min, double max)
	{
		return Math.random()*(max-min)+min;
	}
	public static void checkBounds3D()
	{
		double[] pos = new double[3];
		for (int particle = 0; particle <= getLastFull(); particle++)
		{
			pos = vars.particleList[particle].position;
			if(pos[0]<-vars.clipping || pos[0]>vars.clipping || pos[1]<-vars.clipping || pos[1]>vars.clipping || pos[2]<-vars.clipping || pos[2]>vars.clipping)
			{
				System.out.println("Removed "+particle);
				removeParticle(particle);
			}
		}
	}
	public static boolean collide(int p1, int p2)
	{
		if (distance(vars.particleList[p1].position, vars.particleList[p2].position) <= vars.particleList[p1].radius + vars.particleList[p2].radius && vars.particleList[p1].mass != 0 && vars.particleList[p2].mass != 0)
		{
			return true;
		}
		return false;
	}
	public static void checkCollide()
	{
		for (int p1 = 0; p1 <= getLastFull(); p1++)
		{
			for (int p2 = p1+1; p2 <= getLastFull(); p2++)
			{
				if (collide(p1, p2))
				{
					System.out.println(p1+" ->|<- "+p2);
					particle temp = new particle();
					temp.position = vecDiv(vecAdd(vecMul(vars.particleList[p1].position,vars.particleList[p1].mass),vecMul(vars.particleList[p2].position,vars.particleList[p2].mass)), vars.particleList[p1].mass+vars.particleList[p2].mass);
					temp.velocity = vecDiv(vecAdd(vecMul(vars.particleList[p1].velocity,vars.particleList[p1].mass),vecMul(vars.particleList[p2].velocity,vars.particleList[p2].mass)), vars.particleList[p1].mass+vars.particleList[p2].mass);
					temp.mass = vars.particleList[p1].mass+vars.particleList[p2].mass;
					temp.radius = Math.cbrt((3*temp.mass)/(4*Math.PI*vars.density));
					temp.color = vecDiv(vecAdd(vecMul(vars.particleList[p1].color,vars.particleList[p1].mass),vecMul(vars.particleList[p2].color,vars.particleList[p2].mass)), vars.particleList[p1].mass+vars.particleList[p2].mass);
					vars.particleList[p1] = temp;
					removeParticle(p2);
				}
			}
		}
	}
	public static void checkBounce()
	{
		for (int p1 = 0; p1 <= getLastFull(); p1++)
		{
			for (int p2 = p1+1; p2 <= getLastFull(); p2++)
			{
				if (collide(p1, p2))
				{
					System.out.println(p1+" ->|<- "+p2);
					particle temp1 = new particle();
					particle temp2 = new particle();
					temp1.mass = 0xcafebabe;
					temp2.mass = 0xdeadbeef;
				}
			}
		}
	}
	public static void readyExecutor()
	{
		vars.executor.shutdown();
		while (!vars.executor.isTerminated())
		{
			//
		}
		vars.executor = Executors.newFixedThreadPool(vars.numProcessors);
	}
	public static String fileGen(int frame)
	{
		String rv = Integer.toString(frame);
		while (rv.length() < 4)
		{
			rv = "0"+rv;
		}
		rv = "data/"+rv+".dat";
		return rv;
	}
	public static String imgGen(int frame)
	{
		String rv = Integer.toString(frame);
		while (rv.length() < 4)
		{
			rv = "0"+rv;
		}
		rv = "img/"+rv+".png";
		return rv;
	}
	public static void dumpData(int frame) throws Exception
	{
		String filename = fileGen(frame);
		FileOutputStream fos = new FileOutputStream(filename);
		ObjectOutputStream out = new ObjectOutputStream(fos);
		out.writeObject(vars.particleList);
		out.close();
	}
	public static void deleteOld() throws Exception
	{
		String filename;
		File target;
		for (int frame = 0; frame < vars.totalFrames; frame++)
		{
			filename = fileGen(frame);
			target = new File(filename);
			if (!target.exists())
			{
				return;
			}
			if (target.delete())
			{
			}
			else
			{
				System.err.println("Failed to delete " + filename);
			}
			filename = imgGen(frame);
			target = new File(filename);
			if (!target.exists())
			{
				return;
			}
			if (target.delete())
			{
			}
			else
			{
				System.err.println("Failed to delete " + filename);
			}
		}
	}
	public static double[] convert(double[] pos, int height, int width)
	{
		double[] rv = {width/2.0, height/2.0};
		rv[0] = rv[0] - (Math.sqrt(3)*pos[0]/2.0) + (Math.sqrt(3)*pos[1]/2.0);
		rv[1] = rv[1] - pos[2] - pos[0]/2 - pos[1]/2;
		return rv;
	}
	public static void write(BufferedImage image, int frame) throws Exception
	{
		String filename = Integer.toString(frame);
		while (filename.length() < 4)
		{
			filename = "0"+filename;
		}
		filename = "img/"+filename+".png";
		File file = new File(filename);
		ImageIO.write(image, "png", file);
	}
	public static void draw(int frame) throws Exception
	{
		double[] center = new double[2];
		double x, y;
		BufferedImage graphicsObj = new BufferedImage(vars.width, vars.height, BufferedImage.TYPE_INT_RGB);
		Graphics2D img = graphicsObj.createGraphics();
		for (int i = 0; i < getLastFull(); i++)
		{
			if (vars.particleList[i].mass != 0)
			{
				img.setColor(new Color((int)vars.particleList[i].color[0],(int)vars.particleList[i].color[1], (int)vars.particleList[i].color[2]));
				center = convert(vars.particleList[i].position, vars.height, vars.width);
				x = center[0] - vars.particleList[i].radius;
				y = center[1] - vars.particleList[i].radius;
				img.fill(new Ellipse2D.Double(x, y, vars.particleList[i].radius * 2, vars.particleList[i].radius * 2));
				continue;
			}
			break;
		}
		write(graphicsObj, frame);
		img.dispose();
	}
	public static void main(String[] args) throws Exception
	{
		System.out.println("Resetting...");
		deleteOld();
		System.out.println("Initializing list...");
		initList();
		System.out.println("Populating list...");
		populateList();
		System.out.println("Done init.");
		for (vars.frame = 0; vars.frame < vars.totalFrames; vars.frame++)
		{
			System.out.println("Frame "+vars.frame);
			System.out.println("Accelerations updating...");
			resetAcc();
			for (int p1 = 0; p1 < getLastFull(); p1++)
			{
				vars.executor.execute(new updateAcc(p1));
			}
			readyExecutor();
			System.out.println("Velocities updating...");
			for (int particle = 0; particle <= getLastFull(); particle++)
			{
				vars.executor.execute(new updateVel(particle));
			}
			readyExecutor();
			System.out.println("Positions updating...");
			for (int particle = 0; particle <= getLastFull(); particle++)
			{
				vars.executor.execute(new updatePos(particle));
			}
			readyExecutor();
			System.out.println("Checking boundaries...");
			checkBounds3D();
			System.out.println("Organizing list...");
			arrange();
			if (vars.particleList[0].mass == 0)
			{
				break;
			}
			if (getLastFull() == 1 && vars.finished == false)
			{
				vars.finished = true;
				vars.totalFrames = vars.frame + 100;
			}
			System.out.println("Dumping data...");
			dumpData(vars.frame);
			System.out.println("Writing image...");
			draw(vars.frame);
		}
		vars.executor.shutdown();
	}
}